Support flashing in Secure Download Mode#990
Conversation
e67bec9 to
5398109
Compare
5d4f443 to
067f040
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds support for flashing ESP32 devices in Secure Download Mode, which uses ROM UART download with restricted command availability. The implementation avoids using compression (which requires stub loader commands not available in Secure Download Mode) and instead uses standard FlashBegin/FlashData/FlashEnd commands. Additionally, the PR implements bootloader protection to prevent writes below address 0x8000 and disables watchdog disable, verify, and skip operations which rely on restricted commands.
Key Changes:
- Implements non-compressed flash operations using FlashBegin/FlashData/FlashEnd for Secure Download Mode
- Adds bootloader protection to reject writes below 0x8000 when Secure Download Mode is enabled
- Skips watchdog disable, verify, and skip operations in Secure Download Mode
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| espflash/src/target/flash_target/esp32.rs | Implements dual-path flashing logic (compressed with stub vs uncompressed without stub), updates watchdog disable to skip in Secure Download Mode, renames need_deflate_end to need_flash_end |
| espflash/src/flasher/mod.rs | Adds bootloader protection checks and warning messages for Secure Download Mode in both image and binary flashing methods |
| espflash/src/error.rs | Adds new error variant for bootloader protection violation |
| CHANGELOG.md | Documents the new Secure Download Mode support feature |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
espflash/src/flasher/mod.rs
Outdated
| for segment in image_format.clone().flash_segments() { | ||
| if segment.addr < BOOTLOADER_PROTECTION_ADDR { | ||
| return Err(Error::SecureDownloadBootloaderProtection); | ||
| } | ||
| } |
There was a problem hiding this comment.
The image_format is cloned here to iterate segments for address validation, then the original is consumed later at line 964. This could be optimized by collecting segments once into a Vec, validating addresses, then flashing from the Vec. However, this would require changing how flash_segments works since it consumes self.
Consider refactoring to collect segments once and reuse them, or provide a non-consuming iterator method for flash_segments to avoid the clone.
There was a problem hiding this comment.
I think this is valid, but also begs the question whether we should split the check out into its own fn, as its duplicated in more than one place.
There was a problem hiding this comment.
I agree, I ended up not breaking it out initially as load_image_to_flash() and write_bins_to_flash() are close to duplicates right now. I'm happy break this out into a new fn and open a new PR to track a unification of these and introduce a --force flag if desired).
There was a problem hiding this comment.
Maybe we can do the deduplication in this PR and leave the --force flag for an upcoming PR?
| let mut data = segment.data.to_vec(); | ||
| let padding = (flash_write_size - (data.len() % flash_write_size)) % flash_write_size; | ||
| data.extend(repeat_n(0xff, padding)); | ||
| data |
There was a problem hiding this comment.
When compression is disabled, the data is manually padded here with 0xff bytes to align to flash_write_size. However, at line 229, the FlashData command is also configured with pad_to: flash_write_size, which causes the data_command function to apply padding again. This results in double padding.
The manual padding should be removed since the FlashData command handles padding automatically via the pad_to parameter.
| })?; | ||
| Ok(()) | ||
| }, | ||
| )?; |
There was a problem hiding this comment.
When FlashBegin is called (non-compression path), need_flash_end should be set to true to ensure FlashEnd is called in the finish() method. Without this, the flash operation won't be properly completed in Secure Download Mode.
Add self.need_flash_end = true; after the FlashBegin command, similar to what's done for the compression path at line 183.
| )?; | |
| )?; | |
| self.need_flash_end = true; |
There was a problem hiding this comment.
This is absolutely valid, messed up my rebase and caused this.
MabezDev
left a comment
There was a problem hiding this comment.
Sorry, I meant to come back and see copilots output but I got distracted. I've resolved and added some comments on relevant review items.
espflash/src/flasher/mod.rs
Outdated
| for segment in image_format.clone().flash_segments() { | ||
| if segment.addr < BOOTLOADER_PROTECTION_ADDR { | ||
| return Err(Error::SecureDownloadBootloaderProtection); | ||
| } | ||
| } |
There was a problem hiding this comment.
I think this is valid, but also begs the question whether we should split the check out into its own fn, as its duplicated in more than one place.
|
Hey @enody-carter this is a great addition, and really close to complete! Any chance you can come back and push it over the finish line? |
|
Yes! Had a bunch of travel, but will wrap up today. |
067f040 to
40efde2
Compare
|
I believe I have addressed all feedback. Additionally, I tested with the following procedure: On a secure boot enabled ESP32-C6:
On a non-secure boot enabled device:
The tests were performed on macOS 15 and Ubuntu 24.04. |
SergioGasquez
left a comment
There was a problem hiding this comment.
LGTM! Thanks for the contribution, sadly we have no way of currently testing it on HIL, maybe this is something we could improve in the future. This would probably need a rebase once #996 is merged as the required jobs are now updated!
* With secure download enabled, the stub is not utilized so compressed flashing is unavailble. In this situation, use the uncompressed flashing commands. * If secure download is enabled, prevent flashing data to addresses below 0x8000 to prevent overwriting the bootloader and bricking the device
40efde2 to
2571df6
Compare
* With secure download enabled, the stub is not utilized so compressed flashing is unavailble. In this situation, use the uncompressed flashing commands. * If secure download is enabled, prevent flashing data to addresses below 0x8000 to prevent overwriting the bootloader and bricking the device
* With secure download enabled, the stub is not utilized so compressed flashing is unavailble. In this situation, use the uncompressed flashing commands. * If secure download is enabled, prevent flashing data to addresses below 0x8000 to prevent overwriting the bootloader and bricking the device
|
Hi @enody-carter!
Mind sharing where you found this information? Afaik, in Doing |
|
Hello Sergio, There are a series of checks in esptool cmds.py (starting at line 548). They perform a series of more specific tests (chip specialized, is encryption also enabled on Secure Boot V2), which are all gated behind the --force flag. There is a comment regarding the safety of writing the boot loader:
but during the development / testing of this, I ended up (I think) bricking a couple devices that are using the ESP32-C6 WROOM1 (secure boot & encryption enabled) directly and do not have the BOOT pin tied to a button. This resulted in an invalid boot loader that caused a boot loop, with me unable to put a working bootloader back on. I do not believe any sort of write protection is put in place when secure boot v2 is enabled. In the linked document, only erase is disabled, but I believe you could accomplish the same amount of bricked device via writing an invalid 2nd stage bootloader to flash. There are the various modes of secure download that tend to be enabled at the same time, but from my testing no direct write protection is enabled on the device itself. That being said, I do think there is room for a more granular set of checks regarding the use of secure download mode, secure boot, and flash encryption. I will need to acquire more hardware for testing before I can feel confident implementing these though ! |
But the bootloader protection is only applied when using secure boot (which is not yet implemented in |
|
Ah, I see. This inadvertently coupled SDM and Secure Boot. I apologize, I definitely let the details of my boot process leak into this patch. I am working on broader support for secure boot in espflash, at which point this would be the right change. Want me to open a PR to remove the 0x8000 check until secure boot is supported? |
Thanks, looking forward to the secure boot support <3 ! Dont worry, I can remove the |
Fixes 726
When secure download mode is enabled, the flashing happens via the ROM UART download which does not support all commands. Utilize standard FlashBegin, FlashData, and FlashEnd when the stub is not utilized for flashing. Additionally, do not attempt to disable the watchdog or verify the flash when in secure download mode.
This implementation was based esptool's implementation. I included the write protection for base address < 0x8000 to protect the boot loader. In esptool this is allowed via a --force flag, but no such flag is present in espflash, so I defer to you on what you would like to do regarding this protection.